

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>头盔检测系统-头盔检测</title>
    <link rel="stylesheet" type="text/css" href="EasyUI/themes/default/easyui.css">
    <link rel="stylesheet" type="text/css" href="EasyUI/themes/icon.css">
    <link rel="stylesheet" type="text/css" href="EasyUI/themes/color.css">
    <link rel="stylesheet" type="text/css" href="EasyUI/demo.css">
    <script type="text/javascript" src="EasyUI/jquery.min.js"></script>
    <script type="text/javascript" src="EasyUI/jquery.easyui.min.js"></script>
    <script src="flv.min.js"></script>
    <script src="helmet.js"></script>
    <style>
        body {
            margin: 0;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .drawing-area {
            position: relative;
            width: 1000px;
            height: 550px;
            overflow: hidden;
            margin: 0 auto;
            text-align: center;
            border: 1px solid #ccc; /* 可选：添加边框 */
        }
        .drawing-area img {
            width: 100%;
            height: 100%;
            object-fit: contain;
            display: none;
            z-index: 1; /* 图片在底层 */
        }
        .drawing-area canvas {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 100%;
            height: 100%;
            z-index: 3; /* 画布在最上层 */
            pointer-events: none; /* 允许穿透点击（不影响视频操作） */
        }
        .drawing-area video {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 100%;
            height: 100%;
            object-fit: contain;
            display: none;
            z-index: 2; /* 视频在中间层 */
        }
    </style>
</head>
<body class="easyui-layout" style="background: #00bbee;">

<div id="select" data-options="region:'center',border:false" style="background: #00bbee; text-align: left;  ">
    <div style="padding: 20px;">
        <div style="text-align: left; font-size: 12px;">欢迎 <span id="userNameForWelcome">用户名</span> </div>
        <label for="selectForm" style="font-size: 20px">选择摄像头：</label>
        <form id="selectForm" >
        <label for="province">省：</label>
        <select id="province" style="width: 150px " class="easyui-combobox"  data-options="
        url:'popedom/getProvinces',
        valueField:'id',
        textField:'name',
        onSelect:function(record){
            $('#city').combobox('reload','popedom/getCitiesByProvinceId?provinceId='+record.id);
        }">
        </select>
        <label for="city">市：</label>
        <select id="city" style="width: 150px " class="easyui-combobox" data-options="
        valueField:'id',
        textField:'name',
        onSelect:function(record){
            $('#district').combobox('reload','popedom/getDistrictsByCityId?cityId='+record.id);
        }">
        </select>
        <label for="district">区：</label>
        <select id="district" style="width: 150px " class="easyui-combobox" data-options="
        valueField:'id',
        textField:'name',
        onSelect:function(record){
            $('#block').combobox('reload','popedom/getBlocksByDistrictId?districtId='+record.id);
        }">
        </select>
        <label for="block">块：</label>
        <select id="block" style="width: 150px " class="easyui-combobox" data-options="
        valueField:'id',
        textField:'name',
        onSelect: function(){getCameras()}">
        </select>
        <label for="camera">已选摄像头：</label>
        <select id="camera" class="easyui-combobox"   multiple="true" multiline="true"
                style="width:150px;height:50px;" data-options="
                valueField:'id',
                textField:'id',
                onSelect: function(){showVideo()}">
        </select>
        <button id="confirm" type="button"  style="width: 100px; margin-right: 20px; " onclick="onlineDetect()">识别摄像头</button>
    </form>
    <!-- 图片上传表单 -->
    <label for="uploadForm" style="font-size: 20px">选择本地图片：</label>
</div>
    <form id="uploadForm" enctype="multipart/form-data" style="display: flex; justify-content: center;">
        <input type="file" name="image" accept="image/*" onchange="previewImage(this)" required>
        <button type="button" onclick="localDetect()">识别图片</button>
    </form>

    <!-- 图片显示区域 -->
    <div class="drawing-area"  >
        <img id="previewImage" alt="请上传图片"/>
        <canvas id="resultCanvas" ></canvas>
        <video id="videoElement" controls></video>
    </div>

</div>
<div id="print" data-options="region:'east',split:true,width:300" style="padding: 10px;">
    <form style="height: 80%; width: 100%">
        <label for="outputBox" >识别结果：</label>
        <textarea id="outputBox" style="height: 100%; width: 100%"></textarea>
    </form>
    <div style="text-align: right; position: absolute; bottom: 40px; right: 40px;">
        <button type="button" onclick="exitSystem()">退出系统</button>
    </div>
</div>


<script>
    let flvPlayer = null; // 保存FLV播放器实例

    //页面加载
    $(document).ready(function() {
        welcome().then(user => {
            if (user) {
                document.getElementById('userNameForWelcome').innerText = user.name;
                initWebSocket(user);
                initCameraList(user);
                //initDefaultPopedom(user);
            }
            else
                exitSystem();
        });
        //加载了图片才能绘制检测框
        const img = document.getElementById('previewImage');
        img.src = "/images/transparent_bg.png";
        img.style.display = 'block';
        showImage();
    })

    //在输出框输出结果统计
    function setOutputBox(boxes, cameraId="-1"){
        //计数
        const map = new Map();
        map.set("h",0);//helmet
        map.set("w",0);//without
        map.set("t",0);//two_wheelers
        boxes.forEach(box => {//计数
            map.set(box.l, map.get(box.l)+1);
        });
        //输出框显示
        const outputBox = document.getElementById('outputBox');
        const per = map.get("w")/(map.get("h")+map.get("w")+0.0001);
        if(cameraId !== "-1")outputBox.value += "摄像头"+cameraId+": ";
        outputBox.value += "无头盔数："+map.get("w");
        outputBox.value += " 占比："+toPercentage(per)+"\n";
        outputBox.scrollTop = outputBox.scrollHeight;  // 滚动到最底部
    }
    //打开页面后, 建立websocket连接, 用于接收摄像头实时识别结果
    function initWebSocket(user){
        //创建websocket连接
        const socket = new WebSocket('ws://localhost:8080/results-broadcast?userId=' + user.id);
        socket.onmessage = function (event) {
            // 解析 JSON 格式的消息
            const data = JSON.parse(event.data);
            const cameraId = Object.keys(data)[0]; // 获取第一个键（cameraId）
            const boxes = data[cameraId]; // 获取检测框列表
            // 处理接收到的广播数据
            if (Array.isArray(boxes)) {
                drawBoundingBoxes(boxes);//绘制检测框
                setOutputBox(boxes, cameraId);//输出
            } else {
                console.error('无效的消息格式:', data);
            }
        };

        socket.onerror = function (error) {
            console.error('WebSocket 错误:', error);
        };

        socket.onclose = function () {
            console.log('WebSocket 连接关闭');
        };

    }


    // 使用WebSocket接收FLV流
    function initFlv() {
        const videoElement = document.getElementById('videoElement');

        // 如果已有播放器实例，先销毁
        if (flvPlayer) {
            flvPlayer.unload();
            flvPlayer.detachMediaElement();
            flvPlayer.destroy();
        }

        if(flvjs.isSupported()) {
            flvPlayer = flvjs.createPlayer({
                type: 'flv',
                url: 'ws://localhost:8081/flv',
                isLive: true
            });
            // 添加错误处理
            flvPlayer.on(flvjs.Events.ERROR, (errType, errDetail) => {
                console.error('FLV播放错误:', errType, errDetail);
            });

            // 添加状态监听
            flvPlayer.on(flvjs.Events.METADATA_ARRIVED, () => {
                console.log('FLV元数据到达');
            });
            flvPlayer.attachMediaElement(videoElement);
            // 确保视频元素有正确的监听器
            videoElement.removeEventListener('loadedmetadata', handleVideoMetadata);
            videoElement.addEventListener('loadedmetadata', handleVideoMetadata);

            flvPlayer.load();
            flvPlayer.play().catch(e => {
                console.error('视频播放失败:', e);
                // 处理自动播放被阻止的情况
            });
        }
    }

    // 选好图片时会被调用, 显示图片
    function showImage() {
        const img = document.getElementById('previewImage');
        const video = document.getElementById('videoElement');

        // 显示图片
        img.style.display = 'block';

        // 停止并销毁视频流
        if (flvPlayer) {
            flvPlayer.pause();
            flvPlayer.unload();
            flvPlayer.detachMediaElement();
            flvPlayer.destroy();
            flvPlayer = null;
        }

        // 隐藏视频元素
        video.style.display = 'none';
    }
    // 选好摄像头时会被调用, 显示视频. 待解决: 还需要根据选择结果改进,接收第1个视频流,无则断开连接
    function showVideo() {
        const img = document.getElementById('previewImage');
        const video = document.getElementById('videoElement');
        const canvas = document.getElementById('resultCanvas');

        // 显示视频
        img.style.display = 'none';
        video.style.display = 'block';
        canvas.style.display = 'block'; // 确保画布显示

        // 先初始化画布为默认尺寸
        canvas.width = video.clientWidth;
        canvas.height = video.clientHeight;

        // 然后初始化FLV流
        initFlv();
    }


    // 预览上传的图片
    function previewImage(input) {
        if (input.files && input.files[0]) {
            const reader = new FileReader();
            reader.onload = function(e) {
                const img = document.getElementById('previewImage');
                img.src = e.target.result;
                img.style.display = 'block';
                showImage();
            };
            reader.readAsDataURL(input.files[0]);
        }
    }
    // 提交本地识别请求(单次请求, 无连接)
    function localDetect(){



        const formData = new FormData(document.getElementById('uploadForm'));

        fetch('/detect/yolov10detect', {
            method: 'POST',
            body: formData
        })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    setOutputBox(data.boxes);//在输出框输出文字
                    drawBoundingBoxes(data.boxes);//绘制检测框
                } else {
                    console.log('识别失败: ' + data.message);
                }
            })
            .catch(error => console.error('Error:', error));
    }


    //绘制检测框
    function drawBoundingBoxes(boxes) {
        const canvas = document.getElementById('resultCanvas');
        const ctx = canvas.getContext('2d');
        // 清空画布
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        // 绘制检测框
        boxes.forEach(box1 => {//box1是归一化的框
            //计算框在canvas中的位置
            let box = box1;
            const img = document.getElementById('previewImage');
            const scaleW = canvas.width/img.naturalWidth;//按宽度缩放的比例
            const scaleH = canvas.height/img.naturalHeight;//按高度缩放的比例
            const scale = Math.min(scaleW, scaleH);//缩放比
            const newWidth = Math.floor(img.naturalWidth * scale);//新图宽度
            const newHeight = Math.floor(img.naturalHeight * scale);//新图高度
            if(scaleW< scaleH) {//按照宽缩放
                box.x = box.x*newWidth;
                box.y = box.y*newHeight + (canvas.height-newHeight)/2;
            }
            else{//按照高缩放
                box.x = box.x*newWidth + (canvas.width-newWidth)/2;
                box.y = box.y*newHeight;
            }
            box.w = box.w*newWidth;
            box.h = box.h*newHeight;

            ctx.beginPath();
            //绘制框
            ctx.rect(box.x, box.y, box.w, box.h);
            ctx.lineWidth = 2;
            //绘制文字
            let label = box.l;
            const fontSize = 20; // 字号需与绘制时一致
            ctx.font = `${fontSize}px Arial`;
            //绘制文字底色
            let lt="";
            if(label === "h"){
                ctx.fillStyle = 'blue';
                lt = "helmet";
            }
            else if(label === "w"){
                ctx.fillStyle = 'skyblue';
                lt = "without";
            }
            else {
                ctx.fillStyle = 'white';
                lt = "two_wheels";
            }
            let labelText = lt+' '+ Number(box.c).toFixed(2);
            ctx.fillRect(box.x, box.y - fontSize, ctx.measureText(labelText).width, fontSize);
            //设置框和文字的颜色
            if(label === "h"){
                ctx.strokeStyle = 'blue';
                ctx.fillStyle = 'white';
            }
            else if(label === "w"){
                ctx.strokeStyle = 'skyblue';
                ctx.fillStyle = 'purple';
            }
            else {
                ctx.strokeStyle = 'white';
                ctx.fillStyle = 'purple';
            }
            ctx.stroke();
            ctx.fillText(labelText, box.x,box.y - 5);
        });
    }
    function handleVideoMetadata() {
        const videoElement = document.getElementById('videoElement');
        const canvas = document.getElementById('resultCanvas');

        // 确保视频有有效尺寸
        if (videoElement.videoWidth === 0 || videoElement.videoHeight === 0) {
            console.warn('视频尺寸为0，等待重试...');
            setTimeout(handleVideoMetadata, 100);
            return;
        }
        // 设置画布尺寸与视频帧一致
        canvas.width = videoElement.videoWidth;
        canvas.height = videoElement.videoHeight;

        // 清空画布
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // 调整画布显示尺寸以保持比例
        adjustCanvasSizeByVideo();

        // 移除监听器避免重复执行
        videoElement.removeEventListener('loadedmetadata', handleVideoMetadata);
    }

    //调整画布大小
    function adjustCanvasSizeByVideo() {
        const videoElement = document.getElementById('videoElement');
        const canvas = document.getElementById('resultCanvas');

        // 使用视频元素的显示尺寸而不是容器尺寸
        const displayWidth = videoElement.clientWidth;
        const displayHeight = videoElement.clientHeight;

        // 保持画布显示尺寸与视频一致
        canvas.style.width = displayWidth + 'px';
        canvas.style.height = displayHeight + 'px';
    }
    function adjustCanvasSizeByImage() {
        const img = document.getElementById('previewImage');
        const canvas = document.getElementById('resultCanvas');

        // 获取图片的实际显示尺寸
        const imgWidth = img.clientWidth;
        const imgHeight = img.clientHeight;

        // 设置 Canvas 的像素尺寸
        canvas.width = imgWidth;
        canvas.height = imgHeight;
        // 清空画布
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    // 在图片加载完成后调用
    document.getElementById('previewImage').onload = function() {
        //调整画布大小
        adjustCanvasSizeByImage();
        //清空输出框文本
        const outputBox = document.getElementById('outputBox');
        outputBox.value = '';
    };
    //提交在线检测请求(订阅摄像头, 摄像头实时检测)
    function onlineDetect(){
        //获取选中的摄像头列表
        const cameraList = $('#camera').combobox('getValues').map(Number); // 将字符串数组转换为数字数组
        //新建表单数据
        const formData = new FormData();
        // 检查摄像头列表是否为空
        if (cameraList.length > 0) {
            // 如果摄像头列表不为空，添加每个摄像头 ID
            cameraList.forEach((camera, index) => {
                formData.append('cameras', camera); // 将每个摄像头 ID 添加到表单数据中
            });
        } else {
            // 如果摄像头列表为空，可以添加一个空数组
            formData.append('cameras', []); //
        }
        //发起请求
        fetch('/camera/subscribe', {
            method: 'POST',
            body: formData // 发送表单数据
        })
            .then(response => response.json())
            .then(data => {
                if (! data.success) {
                    alert('订阅失败');
                }
            })
            .catch(error => console.error('Error:', error));
    }
</script>
</body>
</html>